package com.techblor.lock.library;

import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
 * 基于redis的分布式锁,此锁面对redis集群可能有一定问题，请注意使用
 */
public class RedLock implements DistLock {

    /**
     * 加锁结果
     */
    private static final boolean LOCK_SUCCESS = true;

    /**
     * 锁默认失效时间 单位（ms）
     */
    private static final long DEFAULT_LOCK_TIMEOUT = 30000;

    /**
     * 默认获得锁的超时时间 单位：（ms）
     */
    private static final int DEFAULT_GET_LOCK_TIMEOUT = 10000;

    /**
     * 下次获取锁间隔时间,线程睡眠时间
     */
    private static final int DEFAULT_THREAD_SLEEP = 10;

    private RedisTemplate<String, String> redisTemplate;

    public RedLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * @param key
     * @param acquireTimeout 获得锁的超时时间
     * @param expire         自动失效时间
     * @return
     */
    @Override
    public LockEntry lock(final String key, final int acquireTimeout, final long expire, final int sleep) {
        String value = UUID.randomUUID().toString();
        long end = System.currentTimeMillis() + acquireTimeout;

        while (System.currentTimeMillis() < end) {
            boolean result = setnx(key, value, expire);
            if (LOCK_SUCCESS == result) {
                return new LockEntry(key, value);
            } else {
                // 10毫秒的延迟，避免过多请求,避免可能的活锁
                try {
                    Thread.sleep(sleep);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        return null;
    }

    @Override
    public LockEntry lock(final String key, final int acquireTimeout, final long expire) {
        return lock(key, acquireTimeout, expire, DEFAULT_THREAD_SLEEP);
    }

    @Override
    public LockEntry lock(final String key, final long expire) {
        return lock(key, DEFAULT_GET_LOCK_TIMEOUT, expire);
    }

    @Override
    public LockEntry lock(final String key) {
        return lock(key, DEFAULT_LOCK_TIMEOUT);
    }

    /**
     * 解锁
     *
     * @param lockEntry
     */
    @Override
    public void unlock(final LockEntry lockEntry) {
        if (lockEntry == null) {
            return;
        }

        List<String> keys = Collections.singletonList(lockEntry.getKey());
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, lockEntry.getExpectedValue());
    }

    private boolean set(final String key, final String value, final long time) {

        RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();

        return this.redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Object nativeConnection = connection.getNativeConnection();
            //lettuce连接包下序列化键值，否知无法用默认的ByteArrayCodec解析
            byte[] keyByte = stringRedisSerializer.serialize(key);
            byte[] valueByte = stringRedisSerializer.serialize(value);
            String redisResult = "";
            // lettuce连接包下 redis 单机模式setnx
            if (nativeConnection instanceof RedisAsyncCommands) {
                RedisAsyncCommands commands = (RedisAsyncCommands)nativeConnection;
                //同步方法执行、setnx禁止异步
                redisResult = commands
                        .getStatefulConnection()
                        .sync()
                        .set(keyByte, valueByte, SetArgs.Builder.nx().ex(time));
            }
            // lettuce连接包下 redis 集群模式setnx
            if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                redisResult = clusterAsyncCommands
                        .getStatefulConnection()
                        .sync()
                        .set(keyByte, keyByte, SetArgs.Builder.nx().ex(time));
            }

            return "OK".equalsIgnoreCase(redisResult);
        });
    }

    /**
     * @param key
     * @param value
     * @param milliseconds
     * @return
     */
    private boolean setnx(final String key, final String value, final long milliseconds) {
        return set(key, value, milliseconds);
    }
}
